home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
IRIS Performer 2.2 Friends Demo
/
SGI IRIS Performer 2.2 Friends Demo.iso
/
friends
/
medit
/
pfLoader
/
Internalstuff
/
fileops.c
< prev
next >
Wrap
Text File
|
1997-11-20
|
23KB
|
940 lines
/************************************************************************
Subroutines for reading/writing files
************************************************************************/
/* File format revision */
#define FileRevision 411
flag AsciiMode = FALSE; /* Might do this one day... */
char FileLine[1000], *LineData;
/************************************************************************
Global vars/forward references
************************************************************************/
static float Compatibility; /* File version */
static flag byteswapped = FALSE;
static int MatListSize = 0, /* Size of lists below... */
TexListSize = 0;
static TexturePtr *TextureList;
static MaterialPtr *MaterialList;
static TexturePtr LastTexture;
static MaterialPtr LastMaterial;
static unsigned int LastPolyColour, LastVertexColour;
#if ITSTHEMODELLER
static void DealWithSceneView(reg int id, reg ViewPtr view);
#endif
/************************************************************************
Read/write bytes...
************************************************************************/
static boolean PutByte(reg FILE *f, reg int b)
{
if (!FileError) {
if (fputc(b,f) IS EOF) {
SetError(-1);
return FALSE;
}
return TRUE; /* Ok */
}
else {
return FALSE;
}
}
static boolean GetByte(reg FILE *f, reg int *Result)
{
if (!FileError) {
if ((*Result = fgetc(f)) IS EOF) {
SetError(REACHED_END);
return FALSE;
}
return TRUE; /* Ok */
}
else {
return FALSE;
}
}
/************************************************************************
Read/write a data token...
************************************************************************/
static boolean PutToken(reg FILE *f, reg int token)
{
if (AsciiMode) {
if (fprintf(f, "\n%s", TokenList[token]) ISNT -1) {
return TRUE;
}
return FALSE;
}
else {
while (token > 254) {
if (!PutByte(f, 255)) {
return FALSE;
}
token -= 254;
}
return PutByte(f, token);
}
}
static boolean GetToken(reg FILE *f, reg int *Result)
{
int t;
reg int c, n;
reg flag FoundData;
reg char *l, *d;
if (AsciiMode) {
n = 0;
l = FileLine;
FoundData = FALSE;
do {
if ((c = fgetc(f)) IS EOF) {
SetError(REACHED_END);
return FALSE;
}
if ((c ISNT '\n') AND (n < 999)) {
*l++ = c;
if ((c IS ' ') AND !FoundData) {
d = l;
}
}
} until (c IS '\n');
*l++ = '\0';
LineData = d;
return TRUE;
}
else {
if (!GetByte(f, &t)) {
return FALSE;
}
n = t;
while (t IS 255) {
n--;
if (!GetByte(f, &t)) {
return FALSE;
}
n += t;
}
*Result = n;
return TRUE;
}
}
/************************************************************************
Byte swap a word of data
************************************************************************/
static void ByteSwap(void *word)
{
reg unsigned char t, *w = (unsigned char*)word;
t = w[0]; w[0] = w[3]; w[3] = t;
t = w[1]; w[1] = w[2]; w[2] = t;
}
/************************************************************************
Read/write integers...
************************************************************************/
static boolean PutInt(reg FILE *f, int i)
{
if (!FileError) {
if (fwrite(&i, sizeof(int), 1, f) ISNT 1) {
SetError(-1);
return FALSE;
}
else {
return TRUE; /* Ok */
}
}
else {
return FALSE;
}
}
static boolean GetInt(reg FILE *f, reg int *Result)
{
if (!FileError) {
if (fread(Result, sizeof(int), 1, f) ISNT 1) {
SetError(BAD_FILE);
return FALSE;
}
if (byteswapped) {
ByteSwap(Result);
}
return TRUE; /* Not really ok, but fast and harmless */
}
else {
return FALSE;
}
}
/************************************************************************
Read/write floats...
************************************************************************/
static boolean PutFloats(reg FILE *f, reg float *r, reg unsigned int no)
{
if (!FileError) {
if (fwrite(r,sizeof(float),no,f) ISNT no) {
SetError(-1);
return FALSE;
}
else {
return TRUE; /* Ok */
}
}
else {
return FALSE;
}
}
static boolean GetFloats(reg FILE *f, reg float *Result, reg unsigned int no)
{
reg unsigned int i;
if (!FileError) {
if (fread(Result,sizeof(float),no,f) ISNT no) {
SetError(BAD_FILE);
return FALSE;
}
else {
if (byteswapped) {
for (i=0; i<no; i++) {
ByteSwap(Result+i);
}
}
return TRUE; /* Ok */
}
}
else {
return FALSE;
}
}
/************************************************************************
Read/write reals...
************************************************************************/
static boolean PutReals(reg FILE *f, reg real *r, reg unsigned int no)
{
if (!FileError) {
if (fwrite(r,sizeof(real),no,f) ISNT no) {
SetError(-1);
return FALSE;
}
else {
return TRUE; /* Ok */
}
}
else {
return FALSE;
}
}
#define MaxRealBlock 16
static boolean GetReals(reg FILE *f, reg real *Result, reg unsigned int no)
{
reg unsigned int i, t, *d;
float workspace[MaxRealBlock];
if (!FileError) {
if (Compatibility < 4.01) {
if (GetFloats(f,workspace,no)) {
reg unsigned int i;
reg real *rp = Result;
reg float *fp = workspace;
for (i=0; i<no; i++) {
*rp++ = *fp++;
}
return TRUE; /* Ok */
}
else {
return FALSE;
}
}
else {
if (fread(Result,sizeof(real),no,f) ISNT no) {
SetError(BAD_FILE);
return FALSE;
}
else {
if (byteswapped) {
for (i=0; i<no; i++) {
d = (unsigned int*)Result;
t = d[0];
d[0] = d[1];
d[1] = t;
ByteSwap(d);
ByteSwap(d+1);
Result++;
}
}
return TRUE; /* Ok */
}
}
}
else {
return FALSE;
}
}
#undef MaxRealBlock
/************************************************************************
Read/write strings...
************************************************************************/
static boolean PutString(FILE *f, char *s)
{
if (!FileError) {
while (*s ISNT '\0') {
ifbad (PutByte(f,*s++)) {
return FALSE;
}
}
ifbad (PutByte(f,'\n')) {
return FALSE;
}
return TRUE; /* Ok */
}
else {
return FALSE;
}
}
static boolean GetString(FILE *f, reg char *Result, reg int MaxChars)
{
if (!FileError) {
int c;
reg int i;
if (!fgets(Result,MaxChars,f)) {
SetError(BAD_STRING);
return FALSE;
}
for (i=0; i<MaxChars; i++) {
if (Result[i] IS '\n') {
Result[i] = '\0';
return TRUE;
}
}
while (GetByte(f, &c)) {
if (c IS '\n') {
return TRUE;
}
}
}
return FALSE;
}
/************************************************************************
Read/write sizes of things
************************************************************************/
static void PutSize(FILE *f, int size)
{
if (size > 255) {
PutByte(f,0);
PutInt(f,size);
}
else {
PutByte(f,size);
}
}
static boolean GetSize(reg FILE *f, reg int *n)
{
ifbad(GetByte(f,n)) return FALSE;
if (*n IS 0) {
ifbad(GetInt(f,n)) return FALSE;
}
return TRUE;
}
static void PutSizedInt(reg FILE *f, int i)
{
PutSize(f, sizeof(int));
PutInt(f, i);
}
static boolean GetSizedInt(reg FILE *f, int *i)
{
int s;
if (!GetSize(f, &s) OR (s ISNT sizeof(int))) {
return FALSE;
}
return GetInt(f, i);
}
/************************************************************************
Skip unknown data
************************************************************************/
static boolean SkipData(reg FILE *f, reg int bytes)
{
int i,trash;
for (i=0; i<bytes; i++) {
ifbad(GetByte(f,&trash)) return FALSE;
}
return TRUE;
}
static boolean SkipFuture(reg FILE *f)
{
int n;
ifbad(GetSize(f,&n)) return FALSE; /* Suss size of data... */
ifbad(SkipData(f,n)) return FALSE; /* Skip that much */
return TRUE;
}
/************************************************************************
Change the coordinate system of pre 3.01 files
************************************************************************/
static void TwistReal(reg real *n)
{
if (Compatibility < 3.01) {
reg real temp = n[Y];
n[Y] = -n[Z];
n[Z] = temp;
}
}
static void TwistFloat(reg float *n)
{
if (Compatibility < 3.01) {
reg float temp = n[Y];
n[Y] = -n[Z];
n[Z] = temp;
}
}
/************************************************************************
Read/write a matrix
************************************************************************/
static void PutMatrix(reg FILE *f, reg MatrixPtr m)
{
if (!FileError) {
PutReals(f,m[X],4);
PutReals(f,m[Y],4);
PutReals(f,m[Z],4);
PutReals(f,m[W],4);
}
}
static boolean GetMatrix(reg FILE *f, reg MatrixPtr m)
{
reg int i;
reg real temp;
if (!GetReals(f,m[X],4)) return FALSE;
if (!GetReals(f,m[Y],4)) return FALSE;
if (!GetReals(f,m[Z],4)) return FALSE;
if (!GetReals(f,m[W],4)) return FALSE;
if (Compatibility < 3.01) {
TwistReal(m[X]);
TwistReal(m[Y]);
TwistReal(m[Z]);
TwistReal(m[W]);
for (i=0; i<XYZ; i++) {
temp = m[Y][i];
m[Y][i] = -m[Z][i];
m[Z][i] = temp;
}
}
return TRUE;
}
/************************************************************************
Write a string with its length
************************************************************************/
static void WriteDataString(reg FILE *f, reg int token, reg char *string)
{
PutToken(f,FData);
PutByte(f,token);
PutSize(f,strlen(string)+1);
PutString(f,string);
}
/************************************************************************
Patch up all the pointer references after loading the file
************************************************************************/
static void ObjectReferencePatcher(rconst TreePtr t, rconst void *unused)
{
reg int id;
reg ModelObjectPtr o;
if (t->Type IS InstanceBranch) {
id = t->ObjId;
LoopThroughAllObjects(o) {
if (o->Id IS id) {
t->Obj = o;
if ((Compatibility < 406) AND !strcmp(t->Name, "Object")) {
RenameBranch(t, o->Name);
}
break;
}
}
if (!o) {
SetError(BAD_OBJECT);
}
}
}
typedef struct { TreePtr connect; int id; } CFdata;
static void ConnectionFinder(rconst TreePtr t, rconst void *con)
{
reg CFdata *c = (CFdata*)con;
if (t->BranchId IS c->id) {
c->connect->Connection = t;
}
}
static void ConnectionPatcher(rconst TreePtr e, rconst void *o)
{
CFdata c;
e->Connection = NULL;
if (e->ConnectId ISNT -1) {
c.connect = e;
c.id = e->ConnectId;
ScanTree(((ModelObjectPtr)o)->Tree, ConnectionFinder, &c);
if (!e->Connection) {
ScanTree(((ModelObjectPtr)o)->Engines, ConnectionFinder, &c);
}
}
}
static void PatchReferences(void)
{
reg ModelObjectPtr o;
LoopThroughAllObjects(o) {
if (!o->External) {
ScanTree(o->Tree, ObjectReferencePatcher, NULL);
}
ScanTree(o->Engines, ConnectionPatcher, o);
}
}
/************************************************************************
Read a view structure
************************************************************************/
#if ITSTHEMODELLER
static void ReadView(reg FILE *f)
{
View v;
Matrix proj;
int id, token;
flag hadproj = FALSE;
GetInt(f, &id);
v.HasCentreOfRotation = FALSE;
while (GetToken(f, &token)) {
if (token IS BlockEnd) {
break;
}
else switch (token) {
case FViewThreeDee: GetStuff(GetByte(f,&v.ThreeDee));
break;
case FViewAxis: GetStuff(GetByte(f,&v.Axis));
break;
case FViewViewMatrix: GetStuff(GetMatrix(f,v.ViewMatrix));
break;
case FViewProjectMatrix:hadproj = TRUE;
GetStuff(GetMatrix(f,proj));
break;
case FViewGridMatrix: GetStuff(GetMatrix(f,v.GridMatrix));
break;
case FViewMagnification:GetStuff(GetReals(f,&v.Magnification,1));
if (hadproj AND (Compatibility > 2.99)) {
v.Magnification = 1.0/(v.Magnification*proj[Y][Y]);
}
break;
case FViewCOR: v.HasCentreOfRotation = TRUE;
GetStuff(GetReals(f,v.CentreOfRotation,3));
break;
default: if (!SkipFuture(f)) {
SetError(BAD_VIEW);
}
}
}
v.Valid = TRUE;
if ((id >= 0) AND (id < NoViews)) { /* Object views */
CopyView(DefaultObject2D(id),&v);
}
else if (id IS 0x3d) {
CopyView(DefaultObject3D(),&v);
}
else if ((id >= 0x100) AND (id < (0x100+MaxViewports))) {
CopyView(ObjectView(id-0x100),&v);
}
else if ((id >= 0x180) AND (id < (0x188))) {
CopyView(UserView(id-0x180),&v);
}
else if (Compatibility < 4.0) {
DealWithSceneView(id, &v);
}
}
#else
static void ReadView(reg FILE *f)
{
int token;
real junk[16];
GetInt(f,&token);
while (GetToken(f, &token)) {
if (token IS BlockEnd) {
break;
}
else switch (token) {
case FViewThreeDee: GetStuff(SkipData(f,1)); break;
case FViewAxis: GetStuff(SkipData(f,1)); break;
case FViewViewMatrix: GetStuff(GetReals(f,junk,16)); break;
case FViewProjectMatrix: GetStuff(GetReals(f,junk,16)); break;
case FViewGridMatrix: GetStuff(GetReals(f,junk,16)); break;
case FViewMagnification: GetStuff(GetReals(f,junk,1)); break;
case FViewCOR: GetStuff(GetReals(f,junk,3)); break;
default: if (!SkipFuture(f)) {
SetError(BAD_VIEW);
}
}
}
}
#endif
/************************************************************************
Bounding boxes...
************************************************************************/
static void WriteBoundingBox(reg FILE *f, reg real *min, reg real *max)
{
PutToken(f, FBoundingBox);
PutReals(f, min, 3);
PutReals(f, max, 3);
}
static void ReadBoundingBox(reg FILE *f, reg real *min, reg real *max)
{
GetStuff(GetReals(f, min, 3));
GetStuff(GetReals(f, max, 3));
}
/************************************************************************
Polygon interconnections
************************************************************************/
static int VertexIndex;
#if ITSTHEMODELLER
void WritePolyConnections(FILE *f, TreePtr t)
{
reg int i;
reg TmeshPtr tm;
reg PolygonPtr p;
reg PolygonBeadPtr pb;
LoopThroughBranchesVertices(t, i) {
pb = t->Vertex[i];
PutByte(f, FVertexJoin);
while (pb) {
PutInt(f, pb->Temp);
pb = pb->Touch;
}
PutInt(f, -1);
}
p = t->Unmeshed;
if (p) {
PutByte(f, FUnmeshed);
while (p) {
PutInt(f, p->Flag);
p = p->Mesh;
}
PutInt(f, -1);
}
tm = t->Meshed;
while (tm) {
PutByte(f, FTmesh);
pb = tm->FirstBead;
while (pb) {
PutInt(f, pb->Temp);
pb = pb->Mesh;
}
PutInt(f, -1);
tm = tm->Next;
}
}
#endif
static int VertexListSize = 0;
static PolygonBeadPtr *VertexList = NULL;
static void AddToVertexList(PolygonBeadPtr pb)
{
while (VertexIndex >= VertexListSize) {
if (!VertexListSize) {
VertexListSize = 1024;
VertexList = malloc(VertexListSize*sizeof(PolygonBeadPtr));
}
else {
VertexListSize *= 2;
VertexList = realloc(VertexList, VertexListSize*sizeof(PolygonBeadPtr));
}
}
VertexList[VertexIndex++] = pb;
}
static int PolygonListSize = 0;
static PolygonPtr *PolygonList = NULL;
static void AddToPolygonList(PolygonPtr p)
{
while (VertexIndex >= PolygonListSize) {
if (!PolygonListSize) {
PolygonListSize = 1024;
PolygonList = malloc(PolygonListSize*sizeof(PolygonPtr));
}
else {
PolygonListSize *= 2;
PolygonList = realloc(PolygonList, PolygonListSize*sizeof(PolygonPtr));
}
}
PolygonList[VertexIndex] = p;
}
static void ReadVertexJoin(FILE *f, TreePtr t)
{
int index = 0;
#if ITSTHEMODELLER
reg PolygonBeadPtr pb, first, last = NULL;
if (t->NoVertices IS t->MaxVertices) {
t->MaxVertices += 1000;
if (t->NoVertices IS 0) {
t->Vertex = Allocate(t->MaxVertices*sizeof(PolygonBeadPtr));
}
else {
t->Vertex = Reallocate(t->Vertex, t->MaxVertices*sizeof(PolygonBeadPtr));
}
}
until (index IS -1) {
GetStuff(GetInt(f, &index));
if (index ISNT -1) {
pb = VertexList[index];
pb->Temp = index;
if (last) {
last->Touch = pb;
}
else {
first = pb;
t->Vertex[t->NoVertices++] = first;
}
last = pb;
last->FirstTouch = first;
}
}
if (last) {
last->Touch = NULL;
}
#else
until (index IS -1) {
GetStuff(GetInt(f, &index));
}
#endif
}
static void ReadUnmeshed(FILE *f, TreePtr t)
{
int index = 0;
until (index IS -1) {
GetStuff(GetInt(f, &index));
}
}
static void ReadTmesh(FILE *f, TreePtr t)
{
int lefty, index = 0;
if (Compatibility < 411) { /* Pre 411 files can have leftys */
GetStuff(GetInt(f, &lefty));
}
until (index IS -1) {
GetStuff(GetInt(f, &index));
}
}
/************************************************************************
Go back up a level in the tree
************************************************************************/
static TreePtr SearchForBranch(reg TreePtr t, reg TreePtr parent, reg TreePtr search)
{
reg TreePtr found;
while (t) {
if (t->Across) {
if (found = SearchForBranch(t->Across, t, search)) {
return found;
}
}
if (t IS search) {
return parent;
}
t = t->Down;
}
return NULL;
}
static TreePtr GoUp(reg TreePtr start, reg TreePtr t)
{
#if ITSTHEMODELLER
while (t->Up) { /* Easy in ModelWorks as we have more links */
t = t->Up;
}
t = t->Back;
#else
t = SearchForBranch(start, NULL, t);
#endif
return t;
}
/************************************************************************
Read/write construction vertices
************************************************************************/
static void WriteConstructionVertices(reg FILE *f, reg ModelObjectPtr o)
{
reg PolygonBeadPtr pb = o->FirstConstructionVertex;
while (pb) {
PutByte(f, FVConstruction);
PutReals(f, pb->Position, XYZ);
pb = pb->Next;
}
}
static void ReadConstructionVertex(reg FILE *f, reg ModelObjectPtr o)
{
reg PolygonBeadPtr pb;
pb = NewPolygonBead();
pb->Next = o->FirstConstructionVertex;
o->FirstConstructionVertex = pb;
GetStuff(GetReals(f, pb->Position, XYZ));
}
/************************************************************************
Read an external file reference
************************************************************************/
static void ReadExref(reg ModelObjectPtr o)
{
reg char *name;
reg ModelFilePtr f;
reg ModelObjectPtr top;
ExternalObject = o;
if (name = FindFile(o->File, NULL, FilePath, "objects")) {
Free(o->File);
o->File = CopyOfString(name);
if (f = LoadExternalFile(o->File)) {
o->External = f;
top = f->FirstObject;
while (top) {
if (top->IsTopLevel) {
break;
}
top = top->Next;
}
if (!top) {
top = f->FirstObject;
}
o->Tree = top->Tree;
o->Engines = top->Engines;
}
}
}
/************************************************************************
Read/write motion path data
************************************************************************/
static void WritePathData(reg FILE *f, reg PathPtr p)
{
reg int i, j;
reg PathBeadPtr b;
reg BezierDataPtr s;
PutByte(f, FMotionPath);
b = p->FirstBead;
while (b) {
PutToken(f, FPathBead); PutReals(f, b->Position, 3);
PutToken(f, FBeadRounded); PutInt(f, b->Rounded);
PutToken(f, BlockEnd);
b = b->Next;
}
PutToken(f, FPathBasis); PutInt(f, p->Basis);
PutToken(f, FPathClosed); PutInt(f, p->Closed);
PutToken(f, FPathSmoothing);PutInt(f, p->Smoothing);
#if ITSTHEMODELLER
if (!p->Valid) {
p->Valid = TRUE;
GenerateBezierData(p);
}
#endif
if (p->NoCurves > 0) {
PutToken(f, FBezierData); PutInt(f, p->NoCurves);
PutToken(f, FBezierLength); PutReals(f, &(p->CurveLength), 1);
PutToken(f, FHeadingOffset); PutFloats(f, &(p->HeadingOffset), 1);
PutToken(f, FPitchOffset); PutFloats(f, &(p->PitchOffset), 1);
for (i=0; i<p->NoCurves; i++) {
s = &(p->Curve[i]);
PutToken(f, FBezierSegment); PutInt(f, i);
PutToken(f, FBezierStart); PutReals(f, &(s->Start), 1);
PutToken(f, FBezierLength); PutReals(f, &(s->Length), 1);
for (j=0; j<4; j++) {
PutToken(f, FControlPoint); PutInt(f, j);
PutReals(f, s->ControlPoint[j], XYZ);
}
PutToken(f, FSegmentSize); PutInt(f, s->Subdivision);
PutReals(f, s->l, s->Subdivision);
PutToken(f, BlockEnd);
}
}
PutToken(f, BlockEnd);
}
static void ReadBezierSegment(reg FILE *f, reg PathPtr p)
{
int token, n;
reg flag done = FALSE;
reg BezierDataPtr s;
GetStuff(GetInt(f, &n));
s = &(p->Curve[n]);
until (done OR !GetToken(f, &token)) {
switch (token) {
case BlockEnd: return;
case FBezierStart: GetStuff(GetReals(f, &(s->Start), 1));
break;
case FBezierLength: GetStuff(GetReals(f, &(s->Length), 1));
break;
case FControlPoint: GetStuff(GetInt(f, &n));
GetStuff(GetReals(f, s->ControlPoint[n], XYZ));
break;
case FSegmentSize: GetStuff(GetInt(f, &(s->Subdivision)));
#if ITSTHEMODELLER
s->MaxSubdivision = s->Subdivision;
#endif
s->l = Allocate(s->Subdivision * sizeof(real));
GetStuff(GetReals(f, s->l, s->Subdivision));
break;
default: if (!SkipFuture(f)) {
SetError(BAD_OBJECT);
}
}
}
}
static void ReadPathBead(reg FILE *f, reg PathBeadPtr pb)
{
int token;
reg flag done = FALSE;
until (done OR !GetToken(f, &token)) {
switch (token) {
case BlockEnd: return;
case FBeadRounded: GetStuff(GetInt(f, &(pb->Rounded))); break;
default: if (!SkipFuture(f)) {
SetError(BAD_OBJECT);
}
}
}
}
static void ReadPathData(reg FILE *f, reg PathPtr *p)
{
int token;
Coordinate c;
reg PathPtr new;
reg flag done = FALSE, hadsegs = FALSE;
*p = new = NewPath();
until (done OR !GetToken(f, &token)) {
switch (token) {
case BlockEnd:
#if ITSTHEMODELLER
if (hadsegs AND (Compatibility IS FileRevision)) {
(*p)->Valid = FALSE;
}
#endif
return;
case FPathBead: GetStuff(GetReals(f, c, 3));
ReadPathBead(f, AddBeadToPathEnd(new, c));break;
case FPathBasis: GetStuff(GetInt(f, &(new->Basis))); break;
case FPathClosed: GetStuff(GetInt(f, &(new->Closed))); break;
case FPathSmoothing:GetStuff(GetInt(f, &(new->Smoothing))); break;
case FBezierData: GetStuff(GetInt(f, &(new->NoCurves)));
#if ITSTHEMODELLER
new->MaxCurves = new->NoCurves;
#endif
new->Curve = Allocate(new->NoCurves * sizeof(BezierData));
break;
case FBezierLength: GetStuff(GetReals(f, &(new->CurveLength), 1));
break;
case FHeadingOffset:GetStuff(GetFloats(f, &(new->HeadingOffset), 1));
break;
case FPitchOffset: GetStuff(GetFloats(f, &(new->PitchOffset), 1));
break;
case FBezierSegment:hadsegs = TRUE;
ReadBezierSegment(f, new); break;
default: if (!SkipFuture(f)) {
SetError(BAD_OBJECT);
}
}
}
}